package com.java.st.lib.tools.net;

import com.java.st.lib.tools.mapper.JsonMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.Consts;
import org.apache.http.HttpHost;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class HttpClientUtil {
	private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);

	/**
	 *
	 * @param url
	 *            required
	 * @param customizer
	 *            optional
	 * @return null if required arguments not set or network error, otherwise return
	 *         response entity
	 */
	public static BufferedImage downloadImage(String url, RequestCustomizer customizer) {
		byte[] imgBytes = get(url, byte[].class, customizer);
		if (imgBytes == null) {
			return null;
		}
		ByteArrayInputStream imgIs = new ByteArrayInputStream(imgBytes);

		try {
			return ImageIO.read(imgIs);
		} catch (IOException e) {
			logger.error(ExceptionUtils.getStackTrace(e));
		}
		return null;
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set or network error, otherwise return
	 *         response entity
	 */
	public static <T> T get(String url, Class<T> respType) {
		return get(url, respType, null);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param customizer
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set or network error, otherwise return
	 *         response entity
	 */
	public static <T> T get(String url, Class<T> respType, RequestCustomizer customizer) {
		if (StringUtils.isEmpty(url) || respType == null) {
			return null;
		}

		HttpGet httpGet = new HttpGet(url);
		return doRequest(httpGet, respType, customizer);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param body
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set or network error, otherwise return
	 *         response entity
	 */
	public static <T> T postByte(String url, Class<T> respType, byte[] body) {
		return postByte(url, respType, body, null);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param body
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set or network error, otherwise return
	 *         response entity
	 */
	public static <T> T postJson(String url, Class<T> respType, String body) {
		return postJson(url, respType, body, null);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param params
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set or network error, otherwise return
	 *         response entity
	 */
	public static <T> T postForm(String url, Class<T> respType, Map<String, String> params) {
		return postForm(url, respType, params, null);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param params
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set, otherwise return response entity
	 */
	public static <T> T postMultipart(String url, Class<T> respType, Map<String, Object> params) {
		return postMultipart(url, respType, params, null);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param body
	 *            optional
	 * @param customizer
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set or network error, otherwise return
	 *         response entity
	 */
	public static <T> T postJson(String url, Class<T> respType, String body, RequestCustomizer customizer) {
		if (StringUtils.isEmpty(url) || respType == null) {
			return null;
		}

		HttpPost httpPost = new HttpPost(url);
		if (body != null) {
			httpPost.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
		}
		return doRequest(httpPost, respType, customizer);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param body
	 *            optional
	 * @param customizer
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set or network error, otherwise return
	 *         response entity
	 */
	public static <T> T postByte(String url, Class<T> respType, byte[] body, RequestCustomizer customizer) {
		if (StringUtils.isEmpty(url) || respType == null) {
			return null;
		}

		HttpPost httpPost = new HttpPost(url);
		if (body != null) {
			httpPost.setEntity(new ByteArrayEntity(body));
		}
		return doRequest(httpPost, respType, customizer);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param params
	 *            optional
	 * @param customizer
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set, otherwise return response entity
	 */
	public static <T> T postForm(String url, Class<T> respType, Map<String, String> params,
			RequestCustomizer customizer) {
		if (StringUtils.isEmpty(url) || respType == null) {
			return null;
		}

		HttpPost httpPost = new HttpPost(url);
		if (params != null && params.size() > 0) {
			List<NameValuePair> paramPairs = new ArrayList<NameValuePair>();
			for (Entry<String, String> entry : params.entrySet()) {
				NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue());
				paramPairs.add(pair);
			}
			try {
				httpPost.setEntity(new UrlEncodedFormEntity(paramPairs));
			} catch (UnsupportedEncodingException e) {
				logger.error(ExceptionUtils.getStackTrace(e));
			}
		}
		return doRequest(httpPost, respType, customizer);
	}

	/**
	 *
	 * @param url
	 *            required
	 * @param respType
	 *            required
	 * @param params
	 *            optional
	 * @param customizer
	 *            optional
	 * @param <T>
	 *            response type
	 * @return null if required arguments not set, otherwise return response entity
	 */
	public static <T> T postMultipart(String url, Class<T> respType, Map<String, Object> params,
			RequestCustomizer customizer) {
		if (StringUtils.isEmpty(url) || respType == null) {
			return null;
		}

		HttpPost httpPost = new HttpPost(url);
		if (params != null) {
			MultipartEntityBuilder meBuilder = MultipartEntityBuilder.create();
			Iterator<Entry<String, Object>> it = params.entrySet().iterator();
			while (it.hasNext()) {
				Entry<String, Object> entry = it.next();
				Object valueObj = entry.getValue();
				if (valueObj instanceof File) {
					File file = (File) valueObj;
					FileBody fileBody = new FileBody(file);
					meBuilder.addPart(entry.getKey(), fileBody);
				} else {
					String valueStr = String.valueOf(valueObj);
					meBuilder.addTextBody(entry.getKey(), valueStr, ContentType.create("text/plain", Consts.UTF_8));
				}
			}
			httpPost.setEntity(meBuilder.build());
		}
		return doRequest(httpPost, respType, customizer);
	}

	private static <T> T doRequest(HttpRequestBase request, Class<T> respType, RequestCustomizer customizer) {
		HttpClientContext clientContext = null;
		if (customizer != null) {
			RequestConfig.Builder builder = RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES)
					.setSocketTimeout(10000).setConnectTimeout(10000).setConnectionRequestTimeout(10000);
			clientContext = customizer.customize(builder);
			request.setConfig(builder.build());

			Map<String, String> headers = customizer.headers();
			if (headers != null && headers.size() > 0) {
				for (Entry<String, String> entry : headers.entrySet()) {
					request.addHeader(entry.getKey(), entry.getValue());
				}
			}
		}
		return doRequest(request, respType, clientContext);
	}

	private static <T> T doRequest(HttpRequestBase request, Class<T> respType, HttpClientContext clientContext) {
		CloseableHttpResponse response = null;

		try {
			if (clientContext != null) {
				response = HttpClientInstance.getInstance().execute(request, clientContext);
			} else {
				response = HttpClientInstance.getInstance().execute(request);
			}
			if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() > 300) {
				String errorText = EntityUtils.toString(response.getEntity());
				logger.error(formatURI(request.getURI()) +  errorText);
				return null;
			}

            String respText = null;
			T result = null;
			if (respType == byte[].class) {
				result = (T) EntityUtils.toByteArray(response.getEntity());
			} else if (respType == String.class) {
                result = (T) EntityUtils.toString(response.getEntity(),"UTF-8");
			} else if (respType == HttpRespWithCookie.class) {
                respText = EntityUtils.toString(response.getEntity());
                HttpRespWithCookie respWithCookie = new HttpRespWithCookie();
                respWithCookie.setResp(respText);
                if (clientContext != null && clientContext.getCookieStore() != null) {
                    CookieStore cookieStore = clientContext.getCookieStore();
                    List<Cookie> cookies = cookieStore.getCookies();
                    respWithCookie.setCookies(cookies);
			    }
			    result = (T) respWithCookie;
			} else if (CookieAware.class.isAssignableFrom(respType)) {
                respText = EntityUtils.toString(response.getEntity(),"UTF-8");
			    result = JsonMapper.nonNullMapper().fromJson(respText, respType);
			    String cookie = retrieveCookie(clientContext);
                ((CookieAware)result).setCookie(cookie);
			} else {
                respText = EntityUtils.toString(response.getEntity(),"UTF-8");
                result = JsonMapper.nonNullMapper().fromJson(respText, respType);
            }

            if (result == null) {
                logger.error(formatURI(request.getURI()) +  respText);
            }

            try {
                EntityUtils.consume(response.getEntity());
            } catch (Exception e) {
                logger.error(formatURI(request.getURI()) + formatException(e));
            }

            return result;
		} catch (ClientProtocolException e) {
            logger.error(formatURI(request.getURI()) + formatException(e));
		} catch (IOException e) {
            logger.error(formatURI(request.getURI()) + formatException(e));
		} finally {
			if (response != null) {
                try {
					response.close();
				} catch (IOException e) {
                    logger.error(formatURI(request.getURI()) + formatException(e));
				}
			}
			request.releaseConnection();
		}

		return null;
	}

	private static String formatURI(URI uri) {
	    if (uri == null) {
	        return "";
        }

        String url = uri.toString();
	    if (url.contains("?")) {
	        url = url.substring(0, url.indexOf("?"));
        }
        return "[" + url + "]";
    }

	private static String formatException(Throwable e) {
		if (e == null) {
			return "";
		}

		String stackTrace = ExceptionUtils.getStackTrace(e);
		if (stackTrace == null) {
			return "";
		}

		String errorMsg = stackTrace.replace("\n", "").replace("\r", "");
		if (errorMsg.length() > 100) {
			errorMsg = errorMsg.substring(0, 100);
		}
		return errorMsg;
	}

    private static String retrieveCookie(HttpClientContext clientContext) {
	    if (clientContext == null || clientContext.getCookieStore() == null || clientContext.getCookieStore().getCookies() == null) {
	        return "";
        }

        List<Cookie> cookies = clientContext.getCookieStore().getCookies();
        StringBuilder cookieBuilder = new StringBuilder();
        int i = 0;
        for (Cookie cookie : cookies) {
            cookieBuilder.append(cookie.getName()).append("=").append(cookie.getValue());
            if (i < cookies.size() - 1) {
                cookieBuilder.append("; ");
            }
            i++;
        }
        return cookieBuilder.toString();
    }

	public interface RequestCustomizer {
		/**
		 *
		 * @param builder
		 * @return {@link HttpClientContext}, can be null
		 */
		HttpClientContext customize(RequestConfig.Builder builder);

		Map<String, String> headers();

		boolean hasCookies();
	}

	public static abstract class RequestCustomizerImpl implements RequestCustomizer {
		public HttpClientContext customize(RequestConfig.Builder builder) {
			HttpAuth httpAuth = createProxy(builder);
			HttpClientContext context = null;
			if (httpAuth != null) {
				CredentialsProvider credsProvider = new BasicCredentialsProvider();
				if(StringUtils.isNotEmpty(httpAuth.getProxyHost())
						&& httpAuth.getProxyPort() != null) {
					credsProvider.setCredentials(new AuthScope(httpAuth.getProxyHost(), httpAuth.getProxyPort()),
							new UsernamePasswordCredentials(httpAuth.getProxyUser(), httpAuth.getProxyPass()));
					HttpHost proxy = new HttpHost(httpAuth.getProxyHost(), httpAuth.getProxyPort());
					builder.setProxy(proxy);
				}
				else {
					credsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
							new UsernamePasswordCredentials(httpAuth.getProxyUser(), httpAuth.getProxyPass()));
				}
				context = HttpClientContext.create();
				context.setCredentialsProvider(credsProvider);
			}
			if (hasCookies()) {
				if (context == null) {
					context = HttpClientContext.create();
				}
				context.setCookieStore(new BasicCookieStore());
				builder.setCookieSpec(CookieSpecs.STANDARD_STRICT);
			} else {
				builder.setCookieSpec(CookieSpecs.IGNORE_COOKIES);
			}
			return context;
		}

		@Override
		public Map<String, String> headers() {
			return null;
		}

		@Override
		public boolean hasCookies() {
			return false;
		}

		protected abstract HttpAuth createProxy(RequestConfig.Builder builder);
	}
}
